/*
* lasync.c
* libevent binding for Lua 5.1
* Copyright 2007 Gary Ng<linux@garyng.com>
* This code can be distributed under the LGPL license
*/

#include <stdio.h>
#include <memory.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
#ifndef _WIN32
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/time.h>
#else
#include <windows.h>
#include <time.h>
#endif
#include <errno.h>
#include <event.h>

#define LUA_LIB

#include "lua.h"
#include "lauxlib.h"

typedef struct {
    struct event_base *base;
    int dispatcher;
    int is_select;
    int event_fd_map;
    int event_obj_map;
    int r_cnt;
    int w_cnt;
    int rtab;
    int wtab;
    int o_rtab;
    int o_wtab;
    int dispatcher_idx;
    int fd_map_idx;
    int event_map_idx;
    int auto_requeue;
    int event_objects;
    lua_State *L;
} libevent_context;

typedef struct {
    struct event ev;
    int obj;
    int event_obj;
    int active;
    int fd;
    int want;
    int once;
    double timeout;
    libevent_context *libevent;
} event_context;

libevent_context libevent_handle;

#define MYVERSION	"Libevent for " LUA_VERSION " 0.1"
#define MYEVENT     "Libevent object"
#define MYEVENT_BASE   "Libevent base object"

static void Pevent_call(int fd, short ev, void *arg)
{
 event_context *p = arg;
 libevent_context *libevent = p->libevent;
 lua_State *L = libevent->L;
 int top = lua_gettop(L);
 struct timeval tv, *ptv = NULL;
 double t = p->timeout;
 int loop_dispatcher = libevent->dispatcher_idx > 0 && !lua_isnoneornil(L, libevent->dispatcher_idx);

 if (t >= 0) {
   tv.tv_sec = (int) t;
   tv.tv_usec = (t - tv.tv_sec)*1000000;
   ptv = &tv;
 }

 if (p->active == 0) return;


 lua_rawgeti(L, libevent->event_map_idx, p->event_obj);
 #if 0
 lua_pushinteger(L, p->event_obj);
 lua_gettable(L, libevent->event_map_idx);
 #endif
 if (lua_isuserdata(L,-1) && 
    luaL_checkudata(L,-1,MYEVENT) &&
    lua_touserdata(L,-1) == p) {
    
    unsigned char *actions[4]={NULL, NULL, NULL, NULL};
    int i;

    if (libevent->auto_requeue && !p->once) {
        event_add(&p->ev, ptv);
    }

    if (ev & EV_READ & p->want) actions[0] = "readable";
    if (ev & EV_WRITE & p->want) actions[1] = "writable";
    if (ev & EV_TIMEOUT & p->want) actions[2] = "timeout";
    if (ev & EV_SIGNAL & p->want) actions[3] = "signal";

    for (i = 0; i < sizeof(actions)/sizeof(unsigned char *); i++) {
        unsigned char *action;

        if (action = actions[i]) {
            lua_pushstring(L, action);
            if (loop_dispatcher) {
                lua_gettable(L, libevent->dispatcher_idx);
                if (!lua_isnil(L, -1)) {
                    lua_pushvalue(L, libevent->dispatcher_idx);
                    lua_pushvalue(L, -3);
                    lua_call(L, 2, 0);
                } else {
                    lua_pop(L,1);
                }
            } else {
                lua_gettable(L, -2);
                if (!lua_isnil(L, -1)) {
                    lua_pushvalue(L, -2);
                    lua_call(L, 1, 0);
                } else {
                    lua_pop(L,1);
                }
            }
        }
    }
 } 
 lua_pop(L,1);
}

void Pevent_select(int fd, short ev, void *arg)
{
 event_context *p = arg;
 libevent_context *libevent = p->libevent;
 lua_State *L = libevent->L;
 int top = lua_gettop(L);


 if (fd == -1 || p->active == 0) return;

 lua_pushinteger(L, p->obj);
 lua_gettable(L, libevent->fd_map_idx);
 if (!lua_isnil(L, -1)) {
    if (ev & EV_READ && p->want & EV_READ) {
        p->want &= ~EV_READ;
        if (1 || !lua_isnil(L, -1)) { /* interested */
            libevent->r_cnt++;
            lua_pushinteger(L, libevent->r_cnt);
            lua_pushvalue(L, -2);
            lua_settable(L, libevent->o_rtab);
            lua_pushvalue(L, -1);
            lua_pushinteger(L, libevent->r_cnt);
            lua_settable(L, libevent->o_rtab);
        }
    }
    if (ev & EV_WRITE && p->want & EV_WRITE) {
        p->want &= ~EV_WRITE;
        if (1 || !lua_isnil(L, -1)) { /* interested */
            libevent->w_cnt++;
            lua_pushvalue(L, -1);
            lua_pushinteger(L, libevent->w_cnt);
            lua_settable(L, libevent->o_wtab);
            lua_pushinteger(L, libevent->w_cnt);
            lua_pushvalue(L, -2);
            lua_settable(L, libevent->o_wtab);
        }
    }
 }
 lua_pop(L,1);
}

static void Pevent_cb(int fd, short ev, void *arg)
{
 event_context *p = arg;
 libevent_context *libevent = p->libevent;

 if (libevent->is_select) {
    Pevent_select(fd, ev, arg);
 } else {
    Pevent_call(fd, ev, arg);
 }
}

static event_context *Pevent(lua_State *L, int i, char *who)
{
 if (!lua_isuserdata(L,i)) return NULL;

 if (luaL_checkudata(L,i,MYEVENT)==NULL) {
    return NULL;
    #if 0
    luaL_typerror(L,i,MYEVENT);
    #endif
 }
 return lua_touserdata(L,i);
}


static void Pevent_close(lua_State *L)
{

 event_context *p = Pevent(L, 1,"event close");

 if (p->active) {
    event_del(&p->ev);
    lua_rawgeti(L, LUA_REGISTRYINDEX, p->libevent->event_fd_map); 
    luaL_unref(L, -1, p->obj);
    lua_rawgeti(L, LUA_REGISTRYINDEX, p->libevent->event_obj_map); 
    luaL_unref(L, -1, p->event_obj);
    p->event_obj = -1;
    p->obj = -1;
    lua_pop(L,2);
 }
 p->active = 0;

}

static int Levent_settimeout(lua_State *L)
{
 event_context *p = Pevent(L, 1,"settimeout");
 int t =  luaL_optinteger(L,2, -1);

 if (!p) return 0;

 if ((t < 0 && p->timeout >= 0) && (p->want & (EV_READ | EV_WRITE)) == 0) {
    event_del(&p->ev);
    p->active = 0;
 }
 else if ((t >= 0 && p->timeout < 0) && (p->want & (EV_READ | EV_WRITE)) == 0) {
    struct timeval tv, *ptv = NULL;

     if (t >= 0) {
       tv.tv_sec = (int) t;
       tv.tv_usec = (t - tv.tv_sec)*1000000;
       ptv = &tv;
     }
     if(p->fd >=0) event_add(&p->ev, ptv);  
     else evtimer_add(&p->ev, ptv);
     p->active = 1;
 }
 p->timeout = t;

 return 0;
}

static int Levent_want(lua_State *L)
{
 event_context *p = Pevent(L, 1,"want");
 int want =  luaL_optinteger(L,2, EV_READ | EV_WRITE | EV_TIMEOUT | EV_SIGNAL);

 if (!p) return 0;

 p->want |= want;
 if (p->want & (EV_READ | EV_WRITE) || p->timeout >= 0.0) {
    struct timeval tv, *ptv = NULL;
    double t = p->timeout;

     if (t >= 0) {
       tv.tv_sec = (int) t;
       tv.tv_usec = (t - tv.tv_sec)*1000000;
       ptv = &tv;
     }
     if(p->fd >=0) event_add(&p->ev, ptv);  
     else evtimer_add(&p->ev, ptv);
     p->active = 1;
 }

 return 0;
}

static int Levent_skip(lua_State *L)
{
 event_context *p = Pevent(L, 1,"skip");
 int skip =  luaL_optinteger(L, 2,0);

 if (!p) return 0;
 p->want &= ~skip;
 if ((p->want & (EV_READ | EV_WRITE)) == 0 && p->timeout < 0) {
    event_del(&p->ev);
    p->active = 0;
 }
 return 0;
}

static int Levent_gc(lua_State *L)
{
 event_context *p = Pevent(L, 1,"gc");

 p->libevent->event_objects -=1;
 Pevent_close(L);
 #if 0
 lua_rawgeti(L, LUA_REGISTRYINDEX, p->libevent->event_fd_map); 
 luaL_unref(L, -1, p->obj);
 lua_rawgeti(L, LUA_REGISTRYINDEX, p->libevent->event_obj_map); 
 luaL_unref(L, -1, p->event_obj);
 #endif
 return 0;
}

static int Levent_object(lua_State *L)
{
 event_context *p = Pevent(L, 1,"object");

 if (!p) return 0;

 lua_rawgeti(L, LUA_REGISTRYINDEX, p->libevent->event_fd_map); 
 lua_rawgeti(L, -1, p->obj);
 if (lua_isnil(L, -1) && p->fd >= 0) {
    p->obj = -1;
    Pevent_close(L);
 }

 return 1;
}

static int Levent_getfd(lua_State *L)
{
 event_context *p = Pevent(L, 1,"fd");

 if (!p) return 0;
 lua_pushnumber(L, p->fd);

 return 1;
}

static int Levent_close(lua_State *L)
{
 #if 1
 Pevent_close(L);
 #endif

 return 0;
}

static int Levent_requeue(lua_State *L)
{
 event_context *p = Pevent(L, 1,"requeue");
 struct timeval tv, *ptv = NULL;
 double t = p->timeout;

 if (t >= 0) {
   tv.tv_sec = (int) t;
   tv.tv_usec = (t - tv.tv_sec)*1000000;
   ptv = &tv;
 }
 if(p->fd >=0) event_add(&p->ev, ptv);  
 else evtimer_add(&p->ev, ptv);

 return 0;
}

static int Levent_open(lua_State *L)
{
 event_context *p = Pevent(L, 1,"open");
 double t = luaL_optnumber(L, 2, p->fd < 0 ? 0.1 : -1);
 struct timeval tv, *ptv = NULL;

 if (!p->active && p->event_obj >= 0) {
    if (t >= 0) {
       tv.tv_sec = (int) t;
       tv.tv_usec = (t - tv.tv_sec)*1000000;
       ptv = &tv;
    }
    if(p->fd >= 0) event_add(&p->ev, ptv);  
    else evtimer_add(&p->ev, ptv);
    p->active = 1;
    p->timeout = t;
 }
 
 lua_pushvalue(L,1);

 return 1;
}


static int Levent_base(lua_State *L)
{
    int auto_requeue = luaL_optinteger(L,2,1);
    libevent_context **p = lua_newuserdata(L, sizeof(libevent_context*));
    *p = &libevent_handle;

    (*p)->auto_requeue = auto_requeue;

    luaL_getmetatable(L, MYEVENT_BASE);
    lua_setmetatable(L,-2);

    if (lua_istable(L,1) || lua_isuserdata(L,1)) {
        lua_pushvalue(L,1);
        (*p)->dispatcher = luaL_ref(L, LUA_REGISTRYINDEX);
    } else {
        (*p)->dispatcher = -1;
    }
    return 1;
}

static libevent_context **Pevent_base(lua_State *L, int i)
{
 if (luaL_checkudata(L,i,MYEVENT_BASE)==NULL) {
    luaL_typerror(L,i,MYEVENT_BASE);
 }
 return lua_touserdata(L,i);
}

static int Levent_base_close(lua_State *L)
{
 libevent_context *libevent = *Pevent_base(L, 1);
 if (libevent->dispatcher != -1) {
    luaL_unref(L, LUA_REGISTRYINDEX, libevent->dispatcher);
    libevent->dispatcher = -1;
 }
}

static int Levent_loopexit(lua_State *L)
{
 struct timeval tv, *ptv = NULL;

 libevent_context *libevent = *Pevent_base(L, 1);
 tv.tv_sec = (int) 0;
 tv.tv_usec = 0;
 ptv = &tv;
 
 event_base_loopexit(libevent->base, NULL);
 return 0;

}

static int Levent_loop(lua_State *L)
{
 libevent_context *libevent = *Pevent_base(L, 1);
 int once = luaL_optinteger(L,3,0);
 int flags = once ? EVLOOP_NONBLOCK | EVLOOP_ONCE : 0;
 int ret;
 int top = lua_gettop(L);

 libevent->r_cnt = 0;
 libevent->w_cnt = 0;
 libevent->rtab = -1;
 libevent->wtab = -1;
 libevent->o_rtab = -1;
 libevent->o_wtab = -1;
 libevent->L = L;
 libevent->is_select = 0;

 lua_settop(L, 4);
 if (lua_istable(L,2) || lua_isuserdata(L,2)) {
    lua_pushvalue(L,2);
 } else if (libevent->dispatcher > 0)
    lua_rawgeti(L, LUA_REGISTRYINDEX, libevent->dispatcher); /* don't do it in the call back */
 else
    lua_pushnil(L);
 libevent->dispatcher_idx = lua_gettop(L);
 lua_rawgeti(L, LUA_REGISTRYINDEX, libevent->event_fd_map); /* don't do it in the call back */
 libevent->fd_map_idx = lua_gettop(L);

 lua_rawgeti(L, LUA_REGISTRYINDEX, libevent->event_obj_map); /* don't do it in the call back */
 libevent->event_map_idx = lua_gettop(L);

 ret = event_base_loop(libevent->base, flags);
 if (ret < 0) {
   lua_pushnil(L);
   lua_pushnumber(L,ret);
   lua_pushstring(L,strerror(errno));
   return 3;
   }
 else {
   lua_pushnumber(L,1);
  return 1;
 }
}

static int Levent_select(lua_State *L)
{
 libevent_context *libevent = *Pevent_base(L, 1);
 double t = luaL_optnumber(L, 4, -1);
 int flags = EVLOOP_ONCE;
 int i = 0;
 int itab, rtab, wtab;
 event_context tout;
 struct timeval tv, *ptv = NULL;

 if (t >= 0.0) {
    tv.tv_sec = (int) t;
    tv.tv_usec = (t - tv.tv_sec)*1000000;
    tout.libevent = libevent;
    evtimer_set(&tout.ev, Pevent_cb, &tout);
    evtimer_add(&tout.ev, &tv);
    flags |= EVLOOP_NONBLOCK;
    ptv = &tv;
 }

 lua_settop(L, 4);
 lua_rawgeti(L, LUA_REGISTRYINDEX, libevent->event_fd_map); /* don't do it in the call back */
 libevent->fd_map_idx = lua_gettop(L);
 lua_newtable(L); rtab = lua_gettop(L);
 lua_newtable(L); wtab = lua_gettop(L);

 libevent->r_cnt = 0;
 libevent->w_cnt = 0;
 libevent->rtab = 2;
 libevent->wtab = 3;
 libevent->o_rtab = rtab;
 libevent->o_wtab = wtab;
 libevent->L = L;
 libevent->is_select = 1;
 libevent->dispatcher_idx = -1;

 i=0;
 while (++i) {
    int dirty = 0;
    lua_pushnumber(L,i);
    lua_gettable(L, 2);
    if (lua_isnil(L,-1)) {
        lua_pop(L,1);
        break;
    }
    lua_pushstring(L,"dirty");
    lua_gettable(L,-2);
    if (!lua_isnil(L, -1)) {
        lua_pushvalue(L,-2);
        lua_call(L, 1, 1);
        dirty = lua_toboolean(L, -1);
    }
    lua_pop(L,1);
    if (dirty) {
        lua_pushnumber(L, ++libevent->r_cnt);
        lua_pushvalue(L,-2);
        lua_settable(L, libevent->o_rtab);
        lua_pushnumber(L, libevent->r_cnt);
        lua_settable(L, libevent->o_rtab);
    } else {
        lua_pushstring(L,"event");
        lua_gettable(L,-2);
        if (!lua_isnil(L, -1)) {
            event_context *p = Pevent(L, -1, "select"); 
                p->want = EV_READ;
        }
        lua_pop(L,2);
    }
 }

 i=0;
 while (++i) {
    lua_pushnumber(L,i);
    lua_gettable(L, 3);
    if (lua_isnil(L,-1)) {
        lua_pop(L,1);
        break;
    }
    lua_pushstring(L,"event");
    lua_gettable(L,-2);
    if (!lua_isnil(L, -1)) {
        event_context *p = Pevent(L, -1, "select"); 
        p->want |= EV_WRITE;
    }
    lua_pop(L,2);
 }

 event_base_loop(libevent->base, flags);
 if (t >= 0.0) evtimer_del(&tout.ev);
 return 2;
}

static int Levent_state(lua_State *L)
{
 libevent_context *libevent = *Pevent_base(L, 1);
 lua_pushnumber(L, libevent->event_objects);
 return 1;
}

static int Levent(lua_State *L)
{
 libevent_context *libevent = *Pevent_base(L, 1);
 const int fd = luaL_checkinteger(L, 2);
 event_context *p;
 int map_idx,event_map_idx;

 if (lua_isnoneornil(L,3)) luaL_error(L,"lasync.event: parameter #2 must be an object");

 lua_settop(L, 4);

 lua_rawgeti(L, LUA_REGISTRYINDEX, libevent->event_fd_map); 
 map_idx = lua_gettop(L);
 lua_rawgeti(L, LUA_REGISTRYINDEX, libevent->event_obj_map); 
 event_map_idx = lua_gettop(L);


 p = lua_newuserdata(L,sizeof(event_context));

 if (lua_isnoneornil(L,4)) {
    p->once = fd < 0;
 }
 else {
    p->once = luaL_checkinteger(L,4);
 }


 p->timeout = fd < 0 ? 0.1 : -1;
 p->fd = fd;
 p->active = 0;
 if (fd < 0)
    p->want = EV_TIMEOUT;
 else
    p->want = EV_READ | EV_WRITE | EV_SIGNAL | EV_TIMEOUT;

 p->libevent = libevent;
 if (fd >= 0) event_set(&p->ev, fd, EV_READ | EV_WRITE, Pevent_cb, p);
 else evtimer_set(&p->ev, Pevent_cb, p);

 luaL_getmetatable(L, MYEVENT);
 lua_setmetatable(L,-2);
 
 lua_pushvalue(L, 3);
 p->obj = luaL_ref(L, map_idx);
 lua_pushvalue(L, -1);
 p->event_obj = luaL_ref(L, event_map_idx);

 libevent->event_objects += 1;
 return 1;
}

static int Lprobe_read(lua_State *L)
{
 #ifndef _WIN32
 int fd = luaL_checkinteger(L,1);
 int i = 0;
 struct {
    char buf[1];
    struct msghdr msg;
    struct iovec iov;
 } msg;
 int ret;

 memset(&msg,0,sizeof(msg));

 msg.iov.iov_base = msg.buf;
 msg.iov.iov_len = sizeof(msg.buf);
 msg.msg.msg_iov = &msg.iov;
 msg.msg.msg_iovlen = 1;
 for (i = 0; i < 1; i++) {
     ret = sendmsg(fd, &msg.msg, i == 0 ? MSG_PEEK : 0);

     if (ret <= 0) {
        if (errno != EOPNOTSUPP || i == 1) {
            lua_pushnil(L);
            if (errno == EAGAIN || errno == EWOULDBLOCK) lua_pushstring(L,"timeout");
            else lua_pushstring(L,"closed");
            return 2;
        }
     } else {
        lua_pushboolean(L, 1);
        return 1;
     }
 }
 #else
 return 0;
 #endif
}

static int Lprobe(lua_State *L)
{
 #ifndef _WIN32
 int fd = luaL_checkinteger(L,1);
 int i = 0;
 struct {
    char buf[1];
    struct msghdr msg;
    struct iovec iov;
 } msg;
 int ret;

 memset(&msg,0,sizeof(msg));

 msg.iov.iov_base = msg.buf;
 msg.iov.iov_len = sizeof(msg.buf);
 msg.msg.msg_iov = &msg.iov;
 msg.msg.msg_iovlen = 1;
 for (i = 0; i < 2; i++) {
     ret = sendmsg(fd, &msg.msg, i == 0 ? MSG_OOB : 0);

     if (ret <= 0) {
        if (errno != EOPNOTSUPP || i == 1) {
            lua_pushnil(L);
            if (errno == EAGAIN || errno == EWOULDBLOCK) lua_pushstring(L,"timeout");
            else lua_pushstring(L,"closed");
            return 2;
        }
     } else {
        lua_pushboolean(L, 1);
        return 1;
     }
 }
 #else
 return 0;
 #endif
}

static const luaL_reg Rm[] = {
	{ "probe",  Lprobe	},
	{ "probe_read",  Lprobe_read},
	{ "handle",  Levent_base	},
	{ NULL,		NULL	}
};

static const luaL_reg Rbase[] = {
	{ "event",  Levent	},
	{ "state",  Levent_state	},
	{ "select", Levent_select},
	{ "loop", Levent_loop},
	{ "loopexit", Levent_loopexit},
	{ NULL,		NULL	}
};

static const luaL_reg Revent[] = {
	{ "__gc",	Levent_gc	},
  { "object", Levent_object},
  { "getfd", Levent_getfd},
	{ "close",  Levent_close},
	{ "want",  Levent_want},
	{ "settimeout",  Levent_settimeout},
	{ "skip",  Levent_skip},
	{ "open",  Levent_open},
	{ "requeue",  Levent_requeue},
	{ NULL,		NULL	},
};

LUA_API int luaopen_lasync(lua_State *L)
{

 struct event_base *base;

 base = event_init();

 if (!base) {
    luaL_error(L,"luaevent: failed to allocate libevent handle base");
 }

 libevent_handle.base = base;

 luaL_newmetatable(L,MYEVENT);
 lua_pushliteral(L,"__index");
 lua_pushvalue(L,-2);
 lua_settable(L,-3);
 luaL_openlib(L,NULL,Revent,0);

 luaL_newmetatable(L,MYEVENT_BASE);
 lua_pushliteral(L,"__index");
 lua_pushvalue(L,-2);
 lua_settable(L,-3);
 luaL_openlib(L,NULL,Rbase,0);

 lua_newtable(L); /* fdmap */
 lua_newtable(L); /* fdmap metatable {__mode="kv"} */
 lua_pushliteral(L,"__mode");
 lua_pushstring(L,"v");
 lua_settable(L,-3);
 lua_setmetatable(L, -2);
 libevent_handle.event_fd_map = luaL_ref(L, LUA_REGISTRYINDEX);

 lua_newtable(L); /* event map */
 lua_newtable(L); /* event map  metatable {__mode="kv"} */
 lua_pushliteral(L,"__mode");
 lua_pushstring(L,"v");
 lua_settable(L,-3);
 lua_setmetatable(L, -2);
 libevent_handle.event_obj_map = luaL_ref(L, LUA_REGISTRYINDEX);
 
 libevent_handle.L = L;
 libevent_handle.dispatcher = -1;

 luaL_openlib(L,"lasync",Rm,0);
 lua_pushliteral(L,"version");			/** version */
 lua_pushliteral(L,MYVERSION);
 lua_settable(L,-3);
 lua_pushliteral(L,"EV_READ");
 lua_pushinteger(L,EV_READ);
 lua_settable(L,-3);
 lua_pushliteral(L,"EV_WRITE");
 lua_pushinteger(L,EV_WRITE);
 lua_settable(L,-3);
 lua_pushliteral(L,"EV_TIMEOUT");
 lua_pushinteger(L,EV_TIMEOUT);
 lua_settable(L,-3);
 lua_pushliteral(L,"EV_SIGNAL");
 lua_pushinteger(L,EV_SIGNAL);
 lua_settable(L,-3);

 return 1;
}
